﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Linq.Expressions;
using System.Diagnostics.Contracts;
using System.Reflection;

using E = System.Linq.Expressions.Expression;

namespace ReactiveLang.ExpressionConverter.Util
{
    static class ExpressionUtil
    {
        public readonly static E Nothing = E.Constant("Nothing");

        public static readonly E True = E.Constant(true);

        #region Type conversion

        public static E ExpectType(this E expr, Type expectedType)
        {
            Contract.Requires(expr != null && expectedType != null);
            if (expr.Type != expectedType)
                expr = E.Convert(expr, expectedType);
            return expr;
        }
        public static E StripConversion(this E expr)
        {
            Contract.Requires(expr != null);
            Contract.Ensures(expr != null && expr.NodeType !=ExpressionType.Convert);
            while (expr.NodeType == ExpressionType.Convert || expr.NodeType == ExpressionType.ConvertChecked)
                expr = (expr as UnaryExpression).Operand;
            return expr;
        }

        #endregion

        public static Func<T> Compile<T>(E expr)
        {
            Contract.Requires(expr != null);
            Contract.Ensures(Contract.Result<Func<T>>() != null);

            var cnst = expr.StripConversion() as ConstantExpression;
            if (cnst != null)
                return () => (T)cnst.Value;

            if (expr.Type != typeof(T))
                expr = E.Convert(expr, typeof(T));
            return E.Lambda<Func<T>>(expr).Compile();
        }

        #region Replace

        public static E Replace(this E expression, E what, E with)
        {
            Contract.Requires<ArgumentNullException>(expression != null, "expression");
            Contract.Requires<ArgumentNullException>(what != null, "what");
            Contract.Requires<ArgumentNullException>(with != null, "with");
            Contract.Ensures(Contract.Result<E>() != null);
            return expression.Replace(x => x == what ? with : Nothing);
        }
        public static E Replace(this E expression, Func<E, E> selector)
        {
            Contract.Requires<ArgumentNullException>(expression != null, "expression");
            Contract.Requires<ArgumentNullException>(selector != null, "selector");
            Contract.Ensures(Contract.Result<E>() != null);
            return new ExpressionReplacer().Replace(expression, selector);

        }
        public static E Replace(this E expression, E[] what, E[] with)
        {
            Contract.Requires(expression != null && what != null && with != null);
            Contract.Requires(Enumerable.SequenceEqual(
                what.Select(x => x.Type),
                with.Select(x => x.Type)
            ));

            return expression.Replace(x =>
            {
                int pos = Array.IndexOf(what, x);
                if (pos < 0)
                    return ExpressionUtil.Nothing;
                return with[pos];
            });
        }

        #endregion

        #region Expand

        public static IEnumerable<E> Expand(this E expression, Func<E, bool> goDeeper)
        {
            Contract.Requires(expression != null);
            Contract.Requires(goDeeper != null);
            var stack = new ExpressionStack();
            stack.Push(expression);
            while (stack.Count > 0)
            {
                var exp = stack.Pop();
                Contract.Assert(exp != null);
                yield return exp;
                if (goDeeper(exp))
                    stack.PushContent(exp);
            }
        }

        private static IEnumerable<E> NonRecursiveExpandIterator(E expression)
        {
            var stack = new ExpressionStack();
            stack.PushContent(expression);
            while (stack.Count > 0)
                yield return stack.Pop();
        }

        public static IEnumerable<E> Expand(this E expression, bool recursive)
        {
            Contract.Requires(expression != null);

            if (recursive)
                return Expand(expression, x => true);
            else
                return NonRecursiveExpandIterator(expression);
        }

        public static IEnumerable<E> Expand(this E expression)
        {
            return expression.Expand(true);
        }

        #endregion

        public static E SafeAndAlso(E lhs, E rhs)
        {
            if (lhs == null)
                return rhs;
            else if (rhs == null)
                return lhs;
            else
                return E.AndAlso(lhs, rhs);
        }

        public static IEnumerable<E> ExtractMemberArgs(this E expression)
        {
            MemberInfo member;
            return expression.ExtractMemberArgs(out member);
        }
        public static IEnumerable<E> ExtractMemberArgs(this E expression, out MemberInfo member)
        {
            Contract.Requires(expression != null);
            Contract.Ensures(Contract.Result<IEnumerable<E>>() == null || Contract.ForAll(Contract.Result<IEnumerable<E>>(), a => a != null));
            var args = new List<E>();
            switch (expression.NodeType)
            {
                case ExpressionType.MemberAccess:
                    {
                        var memExpr = (MemberExpression)expression;
                        member = memExpr.Member;
                        args.Add(memExpr.Expression);
                        break;
                    }
                case ExpressionType.Index:
                    {
                        var index = (IndexExpression)expression;
                        member = index.Indexer;
                        args.Add(index.Object);
                        args.AddRange(index.Arguments);
                        break;
                    }
                case ExpressionType.Call:
                    {
                        var call = (MethodCallExpression)expression;
                        member = call.Method;
                        args.Add(call.Object);
                        args.AddRange(call.Arguments); break;
                    }
                default:
                    member = null;
                    return null;
            }
            return args.Where(a => a != null);
        }

    }
}
